package brickdestroyer.simulation;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collection;

/**
 * @author Momchil
 */
public class Simulation {

    /**
     * This constant determines the required frame per seconds
     * at which the simulation should get updates. Best results
     * have been observed at 66.
     */
    public static final int SIMULATION_FPS = 66;

    /**
     * This constant determines the width of the simulation 'window'.
     */
    public static final int PLAYABLE_AREA_WIDTH = 600;

    /**
     * This constant determines the height of the simulation 'window'.
     */
    public static final int PLAYABLE_AREA_HEIGHT = 406;

    /*
     * A convenience constant holding the center X coordinate of the
     * simulation 'window'.
     */
    static final int PLAYABLE_AREA_CENTER_X = PLAYABLE_AREA_WIDTH / 2;

    /*
     * A convenience constant holding the center Y coordinate of the
     * simulation 'window'.
     */
    static final int PLAYABLE_AREA_CENTER_Y = PLAYABLE_AREA_HEIGHT / 2;

    /*
     * A convenience constant holding the left-most X coordinate of the
     * simulation 'window'.
     */
    static final int PLAYABLE_AREA_LEFT = 0;

    /*
     * A convenience constant holding the top-most Y coordinte of the
     * simulation 'window'.
     */
    static final int PLAYABLE_AREA_TOP = 0;

    /*
     * A convenience constant holding the right-most X coordinate of the
     * simulation 'window'.
     */
    static final int PLAYABLE_AREA_RIGHT = PLAYABLE_AREA_WIDTH - 1;

    /*
     * A convenience constant holding the bottom-most Y coordinte of the
     * simulation 'window'.
     */
    static final int PLAYABLE_AREA_BOTTOM = PLAYABLE_AREA_HEIGHT - 1;

    /*
     * Determines whether the simulation is running.
     * When this is false. Balls, bricks and physics don't get
     * updated.
     */
    private boolean running = false;

    /*
     * Manager for the slider.
     */
    private final SliderManager sliderManager = new SliderManager();

    /*
     * Manager for all the bricks in the scene.
     */
    private final BrickManager brickManager = new BrickManager(this);

    /*
     * Manager for all balls in the scene.
     */
    private final BallManager ballManager = new BallManager(this);

    /*
     * Manager for checking and reacting to collisions.
     */
    private final CollisionManager collisionManager = new CollisionManager(this);

    /*
     * Manager holding physics constants and coefficients.
     */
    private final PhysicsManager physicsManager = new PhysicsManager();

    /*
     * Manager holding the game's help information. It is also
     * shown and used during game pausing.
     */
    private final HelpManager helpManager = new HelpManager();

    /*
     * This image is used as a back buffer in order to prevent
     * render flickering.
     */
    private final BufferedImage doubleBufferImage =
            new BufferedImage(PLAYABLE_AREA_WIDTH, PLAYABLE_AREA_HEIGHT, BufferedImage.TYPE_INT_ARGB);

    /*
     * Collection of all the simulation listeners.
     */
    private final Collection<SimulationListener> simulationListeners =
            new ArrayList<SimulationListener>();

    
    public Simulation() {
    }

    final SliderManager getSliderManager() {
        return sliderManager;
    }

    final BrickManager getBrickManager() {
        return brickManager;
    }

    final BallManager getBallManager() {
        return ballManager;
    }

    final CollisionManager getCollisionManager() {
        return collisionManager;
    }

    final PhysicsManager getPhysicsManager() {
        return physicsManager;
    }

    final HelpManager getHelpManager() {
        return helpManager;
    }

    /**
     * Returns the current game level.
     * @return current game level.
     */
    public int getLevel() {
        return brickManager.getLevel();
    }

    /**
     * This method adds a simulation listener.
     */
    public void addSimulationListener(SimulationListener listener) {
        simulationListeners.add(listener);
    }

    /**
     * This method removes a simulation listener from the
     * listeners collection.
     */
    public void removeSimulationListener(SimulationListener listener) {
        simulationListeners.remove(listener);
    }

    /*
     * This method is used to notify all listeners of a
     * simulation state change.
     */
    private void notifySimulationStateChange(SimulationState state) {
        for (SimulationListener listener : simulationListeners) {
            listener.onSimulationStateChanged(state);
        }
    }

    /**
     * This method starts the current level.
     */
    public void startGame() {
        if (running) {
            return;
        }
        physicsManager.restoreAll();
        running = true;
        notifySimulationStateChange(SimulationState.START);
    }

    /**
     * This method ends the current level.
     * Warning: This will result in a game over situation.
     */
    public void endGame() {
        brickManager.setLevel(1);
        ballManager.reset();
        running = false;
        notifySimulationStateChange(SimulationState.END);
    }

    /**
     * This method restarts the game and sets it
     * to begin from the first level.
     */
    public void restartGame() {
        brickManager.setLevel(1);
        ballManager.reset();
        running = false;
        notifySimulationStateChange(SimulationState.RESTART);
    }

    /**
     * This method switches the game to the next level.
     * Usually it gets invoked by the simulation itself but
     * is exposed publicly in order to allow for some handicap
     * feature in the future.
     */
    public void nextLevel() {
        brickManager.setLevel(brickManager.getLevel() + 1);
        ballManager.reset();
        running = false;
        notifySimulationStateChange(SimulationState.NEXT_LEVEL);
    }

    /**
     * This is an input method which changes the slider's state.
     */
    public void toggleSlider() {
        sliderManager.toggle();
    }

    /**
     * This is an input method which positions the slider at a specified
     * X coordinate on the screen.
     * @param x new X coordinate of the slider.
     */
    public void setSliderPosition(float x) {
        sliderManager.setPosition(x);
    }

    /**
     * Returns the X coordinate of the slider.
     * @return X coordinate of the slider on the screen.
     */
    public int getSliderPosition() {
        return (int)sliderManager.getPosition();
    }

    /**
     * Changes the game from a paused to an unpaused state and
     * vice versa.
     * @return the pause state of the game after it gets invoked.
     */
    public boolean togglePause() {
        return toggleHelp();
    }

    /**
     * If the help is visible it hides it, otherwise it
     * displays it.
     * @return <code>true</code> if the help is visible after
     * invoke, <code>false</code> otherwise.
     */
    public boolean toggleHelp() {
        return helpManager.toggle();
    }

    /**
     * This method checks if the game is paused.
     * @return <code>true</code> if the game is paused,
     * <code>false</code> otherwise.
     */
    public boolean isPaused() {
        return helpManager.isVisible();
    }

    /**
     * Updates the whole simulation.
     */
    public void update() {
        if (!running || helpManager.isVisible()) {
            return;
        }

        if (brickManager.levelEnded()) {
            nextLevel();
            return;
        }

        /* Scene update */
        physicsManager.update();
        ballManager.update();
        brickManager.update();
        if (ballManager.gameIsOver()) {
            endGame();
        }
    }

    /**
     * Draws the scene on the specified graphics handle.
     * @param g interface to the output graphics.
     */
    public void display(Graphics g) {
        drawScene(doubleBufferImage.createGraphics());
        g.drawImage(doubleBufferImage, 0, 0, null);
    }

    private void drawScene(Graphics2D g) {
        g.setBackground(Color.BLACK);
        g.clearRect(0, 0, PLAYABLE_AREA_WIDTH, PLAYABLE_AREA_HEIGHT);
        ballManager.display(g);
        brickManager.display(g);
        sliderManager.display(g);
        if (helpManager.isVisible()) {
            helpManager.display(g);
        }
    }

}
